/*-
 * Creates useful context for code generator.
 *
 * Copyright (c) 2010 Alex Suhan
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer
 * in this position and unchanged.
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.util.Map;
import java.util.HashMap;
import java.util.Queue;
import java.util.LinkedList;
import java.util.Stack;
import java.util.Collection;

class LayoutBuilder
{
    private ScalaClassTable classes;
    Map<String, ScalaClassLayout> inhTree;
    
    LayoutBuilder(ScalaClassTable classes)
    {
        this.classes = classes;
        inhTree = new HashMap<String, ScalaClassLayout>();
    }
    
    ScalaClassLayout getClassLayout(String name)
    {
        return inhTree.get(name);
    }
    
    Collection<ScalaClassLayout> getClassLayouts()
    {
        return inhTree.values();
    }
    
    private void buildInheritanceTree()
    {
        for (String className: classes.keySet()) {
            ScalaClassLayout classLayout = new ScalaClassLayout(className);
            inhTree.put(className, classLayout);
        }
        
        for (String className: classes.keySet()) {
            ScalaClassInfo classInfo = classes.get(className);
            ScalaClassLayout classLayout = inhTree.get(className);
            ScalaClassLayout parentLayout = inhTree.get(classInfo.getBaseName());
            classLayout.setParent(parentLayout);
            if (parentLayout != null)
                parentLayout.addChild(classLayout);
        }
    }
    
    static boolean isRuntimeClass(String className)
    {
        if (className.equals(ScalaConstants.IntegerClassname))
            return true;
        if (className.equals(ScalaConstants.BooleanClassname))
            return true;
        if (className.equals(ScalaConstants.ConsoleClassname))
            return true;
        if (className.equals(ScalaConstants.StringClassname))
            return true;
        if (className.equals(ScalaConstants.ObjectClassname))
            return true;
        return false;
    }
    
    private void computeLayout(ScalaClassLayout rootClass)
    {
        // breadth-first walk of the inheritance tree for object layout and
        // dispatch tables
        Queue<ScalaClassLayout> frontierBFS = new LinkedList<ScalaClassLayout>();
        frontierBFS.offer(rootClass);
        while (!frontierBFS.isEmpty()) {
            ScalaClassLayout baseNd = frontierBFS.poll();
            for (ScalaClassLayout derivedNd: baseNd.getChildren()) {
                if (isRuntimeClass(derivedNd.name)) {
                    frontierBFS.offer(derivedNd);
                    continue;
                }
                // inherit methods dispatch indices
                int methodOffset = baseNd.nextMethodOffset;
                derivedNd.methodOffset.putAll(baseNd.methodOffset);
                // layout the derived class
                ScalaClassInfo baseInfo = classes.get(baseNd.name);
                ScalaClassInfo derivedInfo = classes.get(derivedNd.name);
                for (String methodName: derivedInfo.getMethodNames()) {
                    ScalaMethodInfo methodInfo = baseInfo.getMethod(methodName);
                    // new method
                    if (methodInfo == null) {
                        ScalaClassLayout.CgenMethodInfo info =
                            new ScalaClassLayout.CgenMethodInfo(methodOffset, derivedNd.name);
                        derivedNd.methodOffset.put(methodName, info);
                        methodOffset++;
                    }
                    else {
                        // method override leaves the old offset entry
                        // in the virtual table
                        int offset = baseNd.methodOffset.get(methodName).offset;
                        ScalaClassLayout.CgenMethodInfo info =
                            new ScalaClassLayout.CgenMethodInfo(offset, baseNd.name);
                        derivedNd.methodOffset.put(methodName, info);
                    }
                }
                derivedNd.nextMethodOffset = methodOffset;
                // TODO: what happens with attribute overriding?
                int attrOffset = baseNd.nextAttrOffset;
                derivedNd.attrOffset.putAll(baseNd.attrOffset);
                for (String attrName: derivedInfo.getAttrNames()) {
                    derivedNd.attrOffset.put(attrName, attrOffset);
                    attrOffset++;
                }
                derivedNd.nextAttrOffset = attrOffset;
                frontierBFS.offer(derivedNd);
            }
        }
    }
    
    void assignTags(ScalaClassLayout rootClass, int tag)
    {
        rootClass.tag = 0;
        // tags must be consistent with the DFS order of the inheritance tree;
        // this is useful for typed case code generation
        Stack<ScalaClassLayout> frontierDFS = new Stack<ScalaClassLayout>();
        frontierDFS.push(rootClass);
        while (!frontierDFS.isEmpty()) {
            ScalaClassLayout baseNd = frontierDFS.pop();
            for (ScalaClassLayout derivedNd: baseNd.getChildren()) {
                if (!isRuntimeClass(derivedNd.name)) {
                    derivedNd.tag = tag;
                    tag++;
                }
                else {
                    if (derivedNd.name.equals(ScalaConstants.ObjectClassname))
                        derivedNd.tag = CgenConstants.OBJECT_TAG;
                    else if (derivedNd.name.equals(ScalaConstants.IntegerClassname))
                        derivedNd.tag = CgenConstants.INTEGER_TAG;
                    else if (derivedNd.name.equals(ScalaConstants.BooleanClassname))
                        derivedNd.tag = CgenConstants.BOOLEAN_TAG;
                    else if (derivedNd.name.equals(ScalaConstants.StringClassname))
                        derivedNd.tag = CgenConstants.STRING_TAG;
                    else if (derivedNd.name.equals(ScalaConstants.ConsoleClassname))
                        derivedNd.tag = CgenConstants.CONSOLE_TAG;
                }
                frontierDFS.push(derivedNd);
            }
        }
    }
    
    void computeLayout()
    {
        buildInheritanceTree();
        
        ScalaClassLayout objectLayout =
            inhTree.get(ScalaConstants.ObjectClassname);
        objectLayout.methodOffset.put("abort",
                new ScalaClassLayout.CgenMethodInfo(0, ScalaConstants.ObjectClassname));
        objectLayout.methodOffset.put("copy",
                new ScalaClassLayout.CgenMethodInfo(1, ScalaConstants.ObjectClassname));
        objectLayout.methodOffset.put("type_name",
                new ScalaClassLayout.CgenMethodInfo(2, ScalaConstants.ObjectClassname));
        objectLayout.nextMethodOffset = 3;
        ScalaClassLayout stringLayout =
            inhTree.get(ScalaConstants.StringClassname);
        stringLayout.methodOffset.putAll(objectLayout.methodOffset);
        stringLayout.methodOffset.put("concat",
                new ScalaClassLayout.CgenMethodInfo(3, ScalaConstants.StringClassname));
        stringLayout.methodOffset.put("length",
                new ScalaClassLayout.CgenMethodInfo(4, ScalaConstants.StringClassname));
        stringLayout.methodOffset.put("substr",
                new ScalaClassLayout.CgenMethodInfo(5, ScalaConstants.StringClassname));
        stringLayout.nextMethodOffset = 6;
        stringLayout.attrOffset.put("val", 0);
        stringLayout.attrOffset.put("str_field", 1);
        stringLayout.nextAttrOffset = 2;
        ScalaClassLayout ioLayout =
            inhTree.get(ScalaConstants.ConsoleClassname);
        ioLayout.methodOffset.putAll(objectLayout.methodOffset);
        ioLayout.methodOffset.put("out_string",
                new ScalaClassLayout.CgenMethodInfo(3, ScalaConstants.ConsoleClassname));
        ioLayout.methodOffset.put("out_int",
                new ScalaClassLayout.CgenMethodInfo(4, ScalaConstants.ConsoleClassname));
        ioLayout.methodOffset.put("in_string",
                new ScalaClassLayout.CgenMethodInfo(5, ScalaConstants.ConsoleClassname));
        ioLayout.methodOffset.put("in_int",
                new ScalaClassLayout.CgenMethodInfo(6, ScalaConstants.ConsoleClassname));
        ioLayout.nextMethodOffset = 7;
        ScalaClassLayout integerLayout =
            inhTree.get(ScalaConstants.IntegerClassname);
        integerLayout.methodOffset.putAll(objectLayout.methodOffset);
        integerLayout.nextMethodOffset = 3;
        integerLayout.attrOffset.put("val", 0);
        integerLayout.nextAttrOffset = 1;
        ScalaClassLayout booleanLayout =
            inhTree.get(ScalaConstants.BooleanClassname);
        booleanLayout.methodOffset.putAll(objectLayout.methodOffset);
        booleanLayout.nextMethodOffset = 3;
        booleanLayout.attrOffset.put("val", 0);
        booleanLayout.nextAttrOffset = 1;
        
        computeLayout(objectLayout);
        assignTags(objectLayout, 5);
    }
    
    @Override
    public String toString()
    {
        return inhTree.toString();
    }
}
